import { ActivationState, Client, type CompatClient, Stomp } from '@stomp/stompjs';
import type { StringKeyObject } from 'my-ts-core/types';
import { WebSocketConnectionState } from 'my-web-core/network';
import SockJS from 'sockjs-client';

import { StompSubscriber } from './stomp-subscriber';

/**
 * It handles stmop connection and its status.
 * It interacts with StompSubscriber for subscription.
 */
export class StompClientService {
  static currentStateValue: WebSocketConnectionState = WebSocketConnectionState.Disconnected;

  private static stompClient: Client | CompatClient | null;

  // Using a map for storing clients instead of a list provides several advantages:
  //
  // Efficient Lookup: Maps provide constant-time lookup complexity, O(1), which means finding a client by its
  // connection object is very fast, regardless of the number of clients connected. In contrast, finding a client
  // in a list would require iterating through the list, resulting in linear-time complexity, O(n), where n is the number of clients.
  //
  // Subscription Management: When clients subscribe to different destinations, using a map allows you to organize
  // subscriptions efficiently. Each destination can map to a set of clients, enabling quick access to clients subscribed
  // to a specific destination. This organization facilitates targeted message delivery based on subscription topics.
  //
  // Easy Removal: Removing a client from a map is straightforward and efficient. With maps, you can directly delete a client
  // entry by its connection object, whereas removing a client from a list involves searching the list for the client and then removing it,
  // resulting in potentially higher time complexity.
  //
  // Avoiding Duplicate Entries: Maps inherently enforce uniqueness in keys. This property ensures that each client connection is
  // uniquely identified, preventing duplicate entries. In contrast, a list allows duplicate entries, which might complicate management,
  // especially if you want to ensure that each client is only subscribed once to a particular destination.
  //
  // While lists can be suitable for maintaining a collection of clients in some scenarios, using a map offers better performance and
  // organization, particularly in scenarios involving subscription management and efficient client lookup, which are common requirements
  // in WebSocket applications. Therefore, using a map is a more optimal choice for storing WebSocket clients, especially when considering
  // scalability and performance aspects.
  private static subscribers: Record<string, StompSubscriber> = {};

  static get currentState(): string {
    return this.currentStateValue;
  }

  /**
   * Initializes a web socket connection and its callbacks.
   *
   * param.endpoint
   * ws://localhost:8080/hello-websocket/stomp for stomp server.
   * /hello-websocket/stomp for socketjs server.
   */
  static init(param: {
    endpoint: string;
    connectHeaders?: StringKeyObject;
    onConnect?: () => void;
    onDisconnect?: () => void;
    onChangeState?: (state: string) => void;
    onWebSocketError?: (err: any) => void;
    onStompError?: (err: any) => void;
  }): void {
    // https://stackoverflow.com/questions/59230634/understanding-connectheaders-when-using-stomp-client
    // https://stomp-js.github.io/api-docs/latest/classes/Client.html#connectHeaders

    if (param.endpoint.startsWith('ws')) {
      this.stompClient = new Client({
        brokerURL: param.endpoint,
        connectHeaders: param.connectHeaders,
      });
    } else {
      const socket = new SockJS('/hello-websocket/stomp');
      this.stompClient = Stomp.over(socket);
    }

    this.stompClient.onConnect = (_) => {
      param.onConnect?.call(this);
    };

    this.stompClient.onDisconnect = (_) => {
      param.onDisconnect?.call(this);
    };

    this.stompClient.onChangeState = (state) => {
      this.currentStateValue = this.getState(state);
      param.onChangeState?.call(this, this.currentStateValue);
    };

    this.stompClient.onWebSocketError = (error) => {
      param.onWebSocketError?.call(this, error);
    };

    this.stompClient.onStompError = (frame) => {
      param.onStompError?.call(this, frame);
    };
  }

  /**
   * Opens web socket connection.
   */
  static connect(): void {
    this.stompClient!.activate();
  }

  /**
   * Clears all stomp clients and their subscriptions and closes web socket connection.
   */
  static async disconnect(): Promise<void> {
    this.clear();
    await this.stompClient!.deactivate();
  }

  /**
   * Creates a StompSubscriber over current web socket connection.
   * Also stores StompSubscriber in local list for auto-clear usage.
   */
  static createSubscriber(): StompSubscriber {
    const stompSubscriber = new StompSubscriber(this.stompClient!);
    this.subscribers[stompSubscriber.id] = stompSubscriber;

    return stompSubscriber;
  }

  /**
   * Clears a StompSubscriber and removes it from local list.
   */
  static remove(stompSubscriber: StompSubscriber): void {
    stompSubscriber.clear();
    delete this.subscribers[stompSubscriber.id];
  }

  /**
   * Clears all StompSubscribers and removes them from local list.
   */
  static clear(): void {
    for (const key in this.subscribers) {
      this.remove(this.subscribers[key]!);
    }
  }

  /**
   * Get current connection state text based on state.
   */
  private static getState(state: ActivationState): WebSocketConnectionState {
    let currentStateValue = WebSocketConnectionState.Disconnected;

    switch (state) {
      case ActivationState.ACTIVE:
        currentStateValue = WebSocketConnectionState.Connected;
        break;
      case ActivationState.DEACTIVATING:
        currentStateValue = WebSocketConnectionState.Disconnecting;
        break;
      case ActivationState.INACTIVE:
        currentStateValue = WebSocketConnectionState.Disconnected;
        break;

      default:
        break;
    }

    return currentStateValue;
  }
}
